package cn.sherer.utils;

import java.io.File;
import java.util.Map;
import java.util.List;
import javax.crypto.Mac;
import java.util.HashMap;
import java.util.ArrayList;
import cn.sherer.beans.weixin.*;
import com.alibaba.fastjson2.JSONArray;
import javax.crypto.spec.SecretKeySpec;
import com.alibaba.fastjson2.JSONObject;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import static cn.sherer.utils.WebUtils.*;
import static java.text.MessageFormat.format;

/**
 * 微信工具类
 * @author sherer
 */
public class WeixinUtils {

    // <editor-fold defaultstate="collapsed" desc="接口地址">

    /** 接口调用凭据地址 */
    public static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}";
    /** 企业微信接口调用凭据地址 */
    public static final String WX_TOKEN_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={0}&corpsecret={1}";

    /** 调用微信JS接口的临时票据地址 */
    public static final String TICKET_URL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={0}&type=jsapi";
    /** 企业微信调用微信JS接口的临时票据地址 */
    public static final String WX_TICKET_URL = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token={0}";

    /** 登录地址（公众号、网站、APP） */
    public static final String LOGIN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={0}&secret={1}&code={2}&grant_type=authorization_code";
    /** 登录地址（小程序） */
    public static final String MP_LOGIN_URL = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";

    /** 刷新令牌地址（公众号、网站、APP） */
    public static final String REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid={0}&grant_type=refresh_token&refresh_token={1}";

    /** 用户信息地址（网站、APP） */
    public static final String USERINFO_URL = "https://api.weixin.qq.com/sns/userinfo?access_token={0}&openid={1}&lang={2}";
    /** 用户信息地址（公众号） */
    public static final String WEB_USERINFO_URL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token={0}&openid={1}&lang={2}";

    /** 用户列表地址（公众号） */
    public static final String USER_LIST_URL = "https://api.weixin.qq.com/cgi-bin/user/get?access_token={0}&next_openid={1}";

    /** * 迁移openid列表地址（公众号） */
    public static final String WEB_CHANGE_OPENID_LIST_URL = "https://api.weixin.qq.com/cgi-bin/changeopenid?access_token={0}";

    /** 获取用户手机号地址（小程序） */
    public static final String GET_USER_PHONE_NUMBER_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={0}";

    /** 获取个人订阅消息模板列表地址 */
    public static final String GET_SUBSCRIBE_TEMPLATE_URL = "https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate?access_token={0}";
    /** 发送订阅消息（公众号） */
    public static final String SEND_SUBSCRIBE_MESSAGE_URL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/bizsend?access_token={0}";
    /** 发送订阅消息（小程序） */
    public static final String MP_SEND_SUBSCRIBE_MESSAGE_URL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={0}";

    /** 发送客服消息地址（公众号） */
    public static final String SEND_CUSTOM_MESSAGE_URL = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token={0}";
    /** 发送客服消息地址（小程序） */
    public static final String MP_SEND_CUSTOM_MESSAGE_URL = "https://api.weixin.qq.com/cgi-bin/message/custom/business/send?access_token={0}";

    /** 发送模板消息地址（公众号） */
    public static final String WEB_SEND_TEMPLATE_MESSAGE_URL = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={0}";

    /** 上传临时素材地址（公众号） */
    public static final String UPLOAD_MEDIA_URL = "https://api.weixin.qq.com/cgi-bin/media/upload?access_token={0}&type={1}";
    /** 上传临时素材地址（小程序） */
    public static final String MP_UPLOAD_MEDIA_URL = "https://api.weixin.qq.com/cgi-bin/media/upload?access_token={0}&type={1}";
    /** 上传临时素材地址（企业微信） */
    public static final String WX_UPLOAD_MEDIA_URL = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={0}&type={1}";

    /** 上传永久地址素材（公众号） */
    public static final String UPLOAD_MATERIAL_URL = "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token={0}&type={1}";

    /** 企业微信读取消息地址 */
    public static final String WX_KF_SYNC_MSG_URL = "https://qyapi.weixin.qq.com/cgi-bin/kf/sync_msg?access_token={0}";
    /** 企业微信发送消息地址 */
    public static final String WX_KF_SEND_MSG_URL = "https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg?access_token={0}";
    /** 企业微信发送特定事件消息地址 */
    public static final String WX_KF_SEND_MSG_ON_EVENT_URL = "https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg_on_event?access_token={0}";

    /** 获取小程序码地址 */
    public static final String GET_WXA_CODE_URL = "https://api.weixin.qq.com/wxa/getwxacode?access_token={0}";
    /** 获取不限制的小程序码地址 */
    public static final String GET_UNLIMITED_WXA_CODE_URL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={0}";
    /** 获取小程序二维码地址 */
    public static final String GET_WXA_QRCODE_URL = "https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token={0}";

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="枚举类型">

    /** 应用类型 */
    public enum AppType {
        /** 小程序 */
        MP,
        /** 公众号 */
        WEB,
        /** 网站 */
        H5,
        /** APP */
        APP,
    }

    /** 语言类型 */
    public enum LangType {
        /** 简体中文 */
        zh_CN,
        /** 繁体中文 */
        zh_TW,
        /** 英文 */
        en,
    }

    /** 客服消息类型 */
    public enum CustomMessageType {
        /** 文本消息（公众号、小程序、企业微信） */
        text,
        /** 图片消息（公众号、小程序、企业微信） */
        image,
        /** 语音消息（公众号、企业微信） */
        voice,
        /** 视频消息（公众号、企业微信） */
        video,
        /** 音乐消息（公众号） */
        music,
        /** 图文链接消息（公众号） */
        news,
        /** 图文素材消息-即将废弃（公众号） */
        mpnews,
        /** 图文文章消息（公众号） */
        mpnewsarticle,
        /** 菜单消息（公众号、企业微信） */
        msgmenu,
        /** 卡券消息（公众号） */
        wxcard,
        /** 小程序卡片（公众号、小程序） */
        miniprogrampage,
        /** 图文链接（小程序、企业微信） */
        link,
        /** 文件消息（企业微信） */
        file,
        /** 小程序卡片（企业微信） */
        miniprogram,
        /** 地理位置消息（企业微信） */
        location,
    }

    /** 媒体类型 */
    public enum MediaType {
        /** 图片（公众号、小程序、企业微信） */
        image,
        /** 语音（公众号、小程序、企业微信） */
        voice,
        /** 视频（公众号、小程序、企业微信） */
        video,
        /** 缩略图（公众号、小程序） */
        thumb,
        /** 文件（企业微信） */
        file,
    }

    /** 小程序版本 */
    public enum MpVersion {
        /** 开发版 */
        develop,
        /** 体验版 */
        trial,
        /** 正式版 */
        release,
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="AccessToken">

    /**
     * 获取接口调用凭据
     * @param appid 应用ID（小程序或公众号）
     * @param secret 应用密钥（小程序或公众号）
     * @return 接口调用凭据
     */
    public static WxToken getAccessToken(String appid, String secret) {
        String result = get(format(TOKEN_URL, appid, secret), WECHAT_AGENT);
        return JSONObject.parseObject(result, WxToken.class);
    }

    /**
     * 获取企业微信接口调用凭据
     * @param corpid 企业ID
     * @param corpsecret 企业应用的凭证密钥
     * @return 接口调用凭据
     */
    public static WxToken getWxAccessToken(String corpid, String corpsecret) {
        String result = get(format(WX_TOKEN_URL, corpid, corpsecret), WECHAT_AGENT);
        return JSONObject.parseObject(result, WxToken.class);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="JS-SDK">

    /**
     * 获取调用微信JS接口的临时票据
     * @param token 接口调用凭据
     * @return 调用微信JS接口的临时票据
     */
    public static WxTicket getTicket(String token) {
        String result = get(format(TICKET_URL, token), WECHAT_AGENT);
        return JSONObject.parseObject(result, WxTicket.class);
    }

    /**
     * 获取企业微信调用微信JS接口的临时票据
     * @param token 接口调用凭据
     * @return 调用微信JS接口的临时票据
     */
    public static WxTicket getWxTicket(String token) {
        String result = get(format(WX_TICKET_URL, token), WECHAT_AGENT);
        return JSONObject.parseObject(result, WxTicket.class);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="登录">

    /**
     * 获取登录凭证（公众号、网站、APP）
     * @param appid 应用ID
     * @param secret 应用密钥
     * @param code 登录时获取的 code
     * @return 登录凭证
     */
    public static WxLogin getLogin(String appid, String secret, String code) {
        return getLogin(AppType.APP, appid, secret, code);
    }

    /**
     * 获取小程序登录凭证
     * @param appid 应用ID
     * @param secret 应用密钥
     * @param code 登录时获取的 code
     * @return 登录凭证
     */
    public static WxLogin getMPLogin(String appid, String secret, String code) {
        return getLogin(AppType.MP, appid, secret, code);
    }

    /**
     * 获取登录凭证
     * @param type 应用类型
     * @param appid 应用ID
     * @param secret 应用密钥
     * @param code 登录时获取的 code
     * @return 登录凭证
     */
    public static WxLogin getLogin(AppType type, String appid, String secret, String code) {
        String url = type == AppType.MP ? MP_LOGIN_URL : LOGIN_URL;
        String result = get(format(url, appid, secret, code), WECHAT_AGENT);
        return JSONObject.parseObject(result, WxLogin.class);
    }

    /**
     * 刷新登录凭证（公众号、网站、APP）
     * @param appid 应用ID
     * @param refreshToken 刷新令牌
     * @return 登录凭证
     */
    public static WxLogin refreshLogin(String appid, String refreshToken) {
        String result = get(format(REFRESH_TOKEN_URL, appid, refreshToken), WECHAT_AGENT);
        return JSONObject.parseObject(result, WxLogin.class);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="用户信息">

    /**
     * 获取用户信息（网站、APP）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @return 用户信息
     */
    public static WxUser getUserInfo(String token, String openid) {
        return getUserInfo(token, openid, LangType.zh_CN);
    }

    /**
     * 获取用户信息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @return 用户信息
     */
    public static WxUser getWebUserInfo(String token, String openid) {
        return getWebUserInfo(token, openid, LangType.zh_CN);
    }

    /**
     * 获取用户信息（网站、APP）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param lang 返回国家地区语言版本，zh_CN 简体，zh_TW 繁体，en 英语
     * @return 用户信息
     */
    public static WxUser getUserInfo(String token, String openid, LangType lang) {
        return getUserInfo(AppType.APP, token, openid, lang);
    }

    /**
     * 获取用户信息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param lang 返回国家地区语言版本，zh_CN 简体，zh_TW 繁体，en 英语
     * @return 用户信息
     */
    public static WxUser getWebUserInfo(String token, String openid, LangType lang) {
        return getUserInfo(AppType.WEB, token, openid, lang);
    }

    /**
     * 获取用户信息（公众号、网站、APP）
     * @param type 应用类型
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param lang 返回国家地区语言版本，zh_CN 简体，zh_TW 繁体，en 英语
     * @return 用户信息
     */
    public static WxUser getUserInfo(AppType type, String token, String openid, LangType lang) {
        String url = type == AppType.WEB ? WEB_USERINFO_URL : USERINFO_URL;
        String result = get(format(url, token, openid, lang.name()), WECHAT_AGENT);
        return JSONObject.parseObject(result, WxUser.class);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="用户列表">

    /**
     * 获取关注公众号用户Openid列表（公众号）
     * @param token 接口调用凭据
     * @return 用户列表
     */
    public static WxUserOpenidResult getUserOpenidList(String token) {
        return getUserOpenidList(token, "");
    }

    /**
     * 获取关注公众号用户Openid列表（公众号）
     * @param token 接口调用凭据
     * @param next_openid 第一个拉取的OPENID，不填默认从头开始拉取
     * @return 用户列表
     */
    public static WxUserOpenidResult getUserOpenidList(String token, String next_openid) {
        String result = get(format(USER_LIST_URL, token, next_openid), WECHAT_AGENT);
        return JSONObject.parseObject(result, WxUserOpenidResult.class);
    }


    /**
     * 获取迁移后的Openid列表（公众号）
     * @param token 接口调用凭据
     * @param from_appid 原账号的appid
     * @param openid_list 需要转换的openid，即第1步中拉取的原账号用户列表，这些必须是旧账号目前关注的才行，否则会出错；一次最多100个，不能多
     * @return 迁移后的Openid列表
     */
    public static WxChangeOpenidList getChangeOpenidList(String token, String from_appid, List<String> openid_list) {
        openid_list = openid_list == null ? new ArrayList<>() : openid_list;
        return getChangeOpenidList(token, from_appid, openid_list.toArray(new String[0]));
    }

    /**
     * 获取迁移后的Openid列表（公众号）
     * @param token 接口调用凭据
     * @param from_appid 原账号的appid
     * @param openid_list 需要转换的openid，即第1步中拉取的原账号用户列表，这些必须是旧账号目前关注的才行，否则会出错；一次最多100个，不能多
     * @return 迁移后的Openid列表
     */
    public static WxChangeOpenidList getChangeOpenidList(String token, String from_appid, String... openid_list) {
        String result = post(format(WEB_CHANGE_OPENID_LIST_URL, token), new JSONObject() {{ put("from_appid", from_appid); put("openid_list", openid_list); }}.toJSONString(), WECHAT_AGENT);
        return JSONObject.parseObject(result, WxChangeOpenidList.class);
    }
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="用户手机号">

    /**
     * 获取用户手机号（小程序）
     * @param token 接口调用凭据
     * @param code 手机号获取凭证
     * @return 用户手机号信息
     */
    public static WxPhoneInfoResult getUserPhone(String token, String code) {
        String result = post(format(GET_USER_PHONE_NUMBER_URL, token), new JSONObject() {{ put("code", code); }}.toJSONString(), WECHAT_AGENT);
        return JSONObject.parseObject(result, WxPhoneInfoResult.class);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="订阅消息">

    /**
     * 获取个人订阅消息模板列表
     * @param token 接口调用凭据
     * @return 订阅消息模板列表
     */
    public static WxSubscribeTemplateResult getSubscribeTemplateList(String token) {
        String result = get(format(GET_SUBSCRIBE_TEMPLATE_URL, token), WECHAT_AGENT);
        return JSONObject.parseObject(result, WxSubscribeTemplateResult.class);
    }

    /**
     * 发送订阅消息（公众号）
     * @param token 接口调用凭据
     * @param data 消息内容
     * @return 发送结果
     */
    public static WxResult sendSubscribeMessage(String token, String data) {
        return sendSubscribeMessage(AppType.WEB, token, data);
    }

    /**
     * 发送订阅消息（小程序）
     * @param token 接口调用凭据
     * @param data 消息内容
     * @return 发送结果
     */
    public static WxResult sendMPSubscribeMessage(String token, String data) {
        return sendSubscribeMessage(AppType.MP, token, data);
    }

    /**
     * 发送订阅消息
     * @param type 应用类型
     * @param token 接口调用凭据
     * @param data 消息内容
     * @return 发送结果
     */
    public static WxResult sendSubscribeMessage(AppType type, String token, String data) {
        String url = type == AppType.MP ? MP_SEND_SUBSCRIBE_MESSAGE_URL : SEND_SUBSCRIBE_MESSAGE_URL;
        String result = post(format(url, token), data, WECHAT_AGENT);
        return JSONObject.parseObject(result, WxResult.class);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="客服消息">

    /**
     * 发送客服文本消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param content 文本内容
     * @return 发送结果
     */
    public static WxResult sendCustomTextMessage(String token, String openid, String content) {
        return sendCustomTextMessage(token, openid, content, null);
    }

    /**
     * 发送客服文本消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param content 文本内容
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendCustomTextMessage(String token, String openid, String content, String kf_account) {
        return sendCustomTextMessage(AppType.WEB, token, openid, content, kf_account);
    }

    /**
     * 发送客服文本消息（小程序）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param content 文本内容
     * @return 发送结果
     */
    public static WxResult sendMPCustomTextMessage(String token, String openid, String content) {
        return sendMPCustomTextMessage(token, openid, content, null);
    }

    /**
     * 发送客服文本消息（小程序）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param content 文本内容
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendMPCustomTextMessage(String token, String openid, String content, String kf_account) {
        return sendCustomTextMessage(AppType.MP, token, openid, content, kf_account);
    }


    /**
     * 发送客服图片消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param media_id 图片的媒体ID
     * @return 发送结果
     */
    public static WxResult sendCustomImageMessage(String token, String openid, String media_id) {
        return sendCustomImageMessage(token, openid, media_id, null);
    }

    /**
     * 发送客服图片消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param media_id 图片的媒体ID
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendCustomImageMessage(String token, String openid, String media_id, String kf_account) {
        return sendCustomImageMessage(AppType.WEB, token, openid, media_id, kf_account);
    }

    /**
     * 发送客服图片消息（小程序）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param media_id 图片的媒体ID
     * @return 发送结果
     */
    public static WxResult sendMPCustomImageMessage(String token, String openid, String media_id) {
        return sendMPCustomImageMessage(token, openid, media_id, null);
    }

    /**
     * 发送客服图片消息（小程序）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param media_id 图片的媒体ID
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendMPCustomImageMessage(String token, String openid, String media_id, String kf_account) {
        return sendCustomImageMessage(AppType.MP, token, openid, media_id, kf_account);
    }


    /**
     * 发送客服语音消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param media_id 语音的媒体ID
     * @return 发送结果
     */
    public static WxResult sendCustomVoiceMessage(String token, String openid, String media_id) {
        return sendCustomVoiceMessage(token, openid, media_id, null);
    }

    /**
     * 发送客服语音消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param media_id 语音的媒体ID
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendCustomVoiceMessage(String token, String openid, String media_id, String kf_account) {
        String data = new JSONObject() {{ put("media_id", media_id); }}.toJSONString();
        return sendCustomMessage(AppType.WEB, token, openid, CustomMessageType.voice, data, kf_account);
    }


    /**
     * 发送客服视频消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param title 视频标题
     * @param description 视频描述
     * @param media_id 视频的媒体ID
     * @param thumb_media_id 视频封面的媒体ID
     * @return 发送结果
     */
    public static WxResult sendCustomVideoMessage(String token, String openid, String title, String description, String media_id, String thumb_media_id) {
        return sendCustomVideoMessage(token, openid, title, description, media_id, thumb_media_id, null);
    }

    /**
     * 发送客服视频消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param title 视频标题
     * @param description 视频描述
     * @param media_id 视频的媒体ID
     * @param thumb_media_id 视频封面的媒体ID
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendCustomVideoMessage(String token, String openid, String title, String description, String media_id, String thumb_media_id, String kf_account) {
        String data = new JSONObject() {{
            put("title", title);
            put("description", description);
            put("media_id", media_id);
            put("thumb_media_id", thumb_media_id);
        }}.toJSONString();
        return sendCustomMessage(AppType.WEB, token, openid, CustomMessageType.video, data, kf_account);
    }


    /**
     * 发送客服音乐消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param title 音乐标题
     * @param description 音乐描述
     * @param musicurl 音乐链接
     * @param hqmusicurl 高品质音乐链接，wifi环境优先使用该链接播放音乐
     * @param thumb_media_id 音乐封面的媒体ID
     * @return 发送结果
     */
    public static WxResult sendCustomMusicMessage(String token, String openid, String title, String description, String musicurl, String hqmusicurl, String thumb_media_id) {
        return sendCustomMusicMessage(token, openid, title, description, musicurl, hqmusicurl, thumb_media_id, null);
    }

    /**
     * 发送客服音乐消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param title 音乐标题
     * @param description 音乐描述
     * @param musicurl 音乐链接
     * @param hqmusicurl 高品质音乐链接，wifi环境优先使用该链接播放音乐
     * @param thumb_media_id 音乐封面的媒体ID
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendCustomMusicMessage(String token, String openid, String title, String description, String musicurl, String hqmusicurl, String thumb_media_id, String kf_account) {
        String data = new JSONObject() {{
            put("title", title);
            put("description", description);
            put("musicurl", musicurl);
            put("hqmusicurl", hqmusicurl);
            put("thumb_media_id", thumb_media_id);
        }}.toJSONString();
        return sendCustomMessage(AppType.WEB, token, openid, CustomMessageType.music, data, kf_account);
    }


    /**
     * 发送客服图文链接消息（公众号）
     * 点击跳转到外链
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param title 图文标题
     * @param description 图文描述
     * @param url 跳转链接（可以外链）
     * @param thumb_url 图文封面链接
     * @return 发送结果
     */
    public static WxResult sendMPCustomLinkMessage(String token, String openid, String title, String description, String url, String thumb_url) {
        return sendMPCustomLinkMessage(token, openid, title, description, url, thumb_url, null);
    }

    /**
     * 发送客服图文链接消息（公众号）
     * 点击跳转到外链
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param title 图文标题
     * @param description 图文描述
     * @param url 跳转链接（可以外链）
     * @param thumb_url 图文封面链接
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendMPCustomLinkMessage(String token, String openid, String title, String description, String url, String thumb_url, String kf_account) {
        String data = new JSONObject() {{
            put("title", title);
            put("description", description);
            put("url", url);
            put("thumb_url", thumb_url);
        }}.toJSONString();
        return sendCustomMessage(AppType.WEB, token, openid, CustomMessageType.link, data, kf_account);
    }


    /**
     * 发送客服图文链接消息（公众号）
     * 点击跳转到外链
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param title 图文标题
     * @param description 图文描述
     * @param url 跳转链接（可以外链）
     * @param picurl 图文封面链接
     * @return 发送结果
     */
    public static WxResult sendCustomNewsMessage(String token, String openid, String title, String description, String url, String picurl) {
        return sendCustomNewsMessage(token, openid, title, description, url, picurl, null);
    }

    /**
     * 发送客服图文链接消息（公众号）
     * 点击跳转到外链
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param title 图文标题
     * @param description 图文描述
     * @param url 跳转链接（可以外链）
     * @param picurl 图文封面链接
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendCustomNewsMessage(String token, String openid, String title, String description, String url, String picurl, String kf_account) {
        String data = new JSONObject() {{
            put("articles", new JSONArray() {{
                add(new JSONObject() {{
                    put("title", title);
                    put("description", description);
                    put("url", url);
                    put("picurl", picurl);
                }});
            }});
        }}.toJSONString();
        return sendCustomMessage(AppType.WEB, token, openid, CustomMessageType.news, data, kf_account);
    }


    /**
     * 发送客服图文素材消息（公众号）
     * 点击跳转到图文消息页面
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param media_id 图文素材的媒体ID
     * @return 发送结果
     */
    public static WxResult sendCustomMPNewsMessage(String token, String openid, String media_id) {
        return sendCustomMPNewsMessage(token, openid, media_id, null);
    }

    /**
     * 发送客服图文素材消息（公众号）
     * 点击跳转到图文消息页面
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param media_id 图文素材的媒体ID
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendCustomMPNewsMessage(String token, String openid, String media_id, String kf_account) {
        String data = new JSONObject() {{ put("media_id", media_id); }}.toJSONString();
        return sendCustomMessage(AppType.WEB, token, openid, CustomMessageType.mpnews, data, kf_account);
    }


    /**
     * 发送客服图文文章消息（公众号）
     * 点击跳转到图文消息页面
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param article_id 图文文章ID，使用通过 “发布” 系列接口得到的
     * @return 发送结果
     */
    public static WxResult sendCustomMPNewsArticleMessage(String token, String openid, String article_id) {
        return sendCustomMPNewsArticleMessage(token, openid, article_id, null);
    }

    /**
     * 发送客服图文文章消息（公众号）
     * 点击跳转到图文消息页面
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param article_id 图文文章ID，使用通过 “发布” 系列接口得到的
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendCustomMPNewsArticleMessage(String token, String openid, String article_id, String kf_account) {
        String data = new JSONObject() {{ put("article_id", article_id); }}.toJSONString();
        return sendCustomMessage(AppType.WEB, token, openid, CustomMessageType.mpnewsarticle, data, kf_account);
    }


    /**
     * 发送客服菜单消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param head_content 菜单头部内容
     * @param tail_content 菜单尾部内容
     * @param list 菜单列表
     * @return 发送结果
     */
    public static WxResult sendCustomMsgMenuMessage(String token, String openid, String head_content, String tail_content, List<WxCustomMsgMenuItem> list) {
        return sendCustomMsgMenuMessage(token, openid, head_content, tail_content, list, null);
    }

    /**
     * 发送客服菜单消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param head_content 菜单头部内容
     * @param tail_content 菜单尾部内容
     * @param list 菜单列表
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendCustomMsgMenuMessage(String token, String openid, String head_content, String tail_content, List<WxCustomMsgMenuItem> list, String kf_account) {
        return sendCustomMsgMenuMessage(token, openid, head_content, tail_content, kf_account, list.toArray(new WxCustomMsgMenuItem[list.size()]));
    }

    /**
     * 发送客服菜单消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param head_content 菜单头部内容
     * @param tail_content 菜单尾部内容
     * @param items 菜单项
     * @return 发送结果
     */
    public static WxResult sendCustomMsgMenuMessage(String token, String openid, String head_content, String tail_content, WxCustomMsgMenuItem ...items) {
        return sendCustomMsgMenuMessage(token, openid, head_content, tail_content, null, items);
    }

    /**
     * 发送客服菜单消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param head_content 菜单头部内容
     * @param tail_content 菜单尾部内容
     * @param kf_account 客服账号
     * @param items 菜单项
     * @return 发送结果
     */
    public static WxResult sendCustomMsgMenuMessage(String token, String openid, String head_content, String tail_content, String kf_account, WxCustomMsgMenuItem ...items) {
        String data = new JSONObject() {{
            put("head_content", head_content);
            put("tail_content", tail_content);
            put("list", items);
        }}.toJSONString();
        return sendCustomMessage(AppType.WEB, token, openid, CustomMessageType.msgmenu, data, kf_account);
    }


    /**
     * 发送客服卡卷消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param card_id 卡卷ID
     * @return 发送结果
     */
    public static WxResult sendCustomWxCardMessage(String token, String openid, String card_id) {
        return sendCustomWxCardMessage(token, openid, card_id, null);
    }

    /**
     * 发送客服卡卷消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param card_id 卡卷ID
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendCustomWxCardMessage(String token, String openid, String card_id, String kf_account) {
        String data = new JSONObject() {{ put("card_id", card_id); }}.toJSONString();
        return sendCustomMessage(AppType.WEB, token, openid, CustomMessageType.wxcard, data, kf_account);
    }


    /**
     * 发送客服小程序卡片消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param title 小程序卡片标题
     * @param appid 小程序的appid
     * @param pagepath 小程序的页面路径
     * @param thumb_media_id 小程序卡片封面的媒体ID
     * @return 发送结果
     */
    public static WxResult sendCustomMiniProgramMessage(String token, String openid, String title, String appid, String pagepath, String thumb_media_id) {
        return sendCustomMiniProgramMessage(token, openid, title, appid, pagepath, thumb_media_id, null);
    }

    /**
     * 发送客服小程序卡片消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param title 小程序卡片标题
     * @param appid 小程序的appid
     * @param pagepath 小程序的页面路径
     * @param thumb_media_id 小程序卡片封面的媒体ID
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendCustomMiniProgramMessage(String token, String openid, String title, String appid, String pagepath, String thumb_media_id, String kf_account) {
        String data = new JSONObject() {{
            put("title", title);
            put("appid", appid);
            put("pagepath", pagepath);
            put("thumb_media_id", thumb_media_id);
        }}.toJSONString();
        return sendCustomMessage(AppType.WEB, token, openid, CustomMessageType.miniprogrampage, data, kf_account);
    }


    /**
     * 发送客服小程序卡片消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param title 小程序卡片标题
     * @param pagepath 小程序的页面路径
     * @param thumb_media_id 小程序卡片封面的媒体ID
     * @return 发送结果
     */
    public static WxResult sendMPCustomMiniProgramMessage(String token, String openid, String title, String pagepath, String thumb_media_id) {
        return sendMPCustomMiniProgramMessage(token, openid, title, pagepath, thumb_media_id, null);
    }

    /**
     * 发送客服小程序卡片消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param title 小程序卡片标题
     * @param pagepath 小程序的页面路径
     * @param thumb_media_id 小程序卡片封面的媒体ID
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendMPCustomMiniProgramMessage(String token, String openid, String title, String pagepath, String thumb_media_id, String kf_account) {
        String data = new JSONObject() {{
            put("title", title);
            put("pagepath", pagepath);
            put("thumb_media_id", thumb_media_id);
        }}.toJSONString();
        return sendCustomMessage(AppType.MP, token, openid, CustomMessageType.miniprogrampage, data, kf_account);
    }


    /**
     * 发送客服消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param msgType 消息类型
     * @param data 消息内容
     * @return 发送结果
     */
    public static WxResult sendCustomMessage(String token, String openid, CustomMessageType msgType, String data) {
        return sendCustomMessage(token, openid, msgType, data, null);
    }

    /**
     * 发送客服消息（公众号）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param msgType 消息类型
     * @param data 消息内容
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendCustomMessage(String token, String openid, CustomMessageType msgType, String data, String kf_account) {
        return sendCustomMessage(AppType.WEB, token, openid, msgType, data, kf_account);
    }

    /**
     * 发送客服消息（小程序）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param msgType 消息类型
     * @param data 消息内容
     * @return 发送结果
     */
    public static WxResult sendMPCustomMessage(String token, String openid, CustomMessageType msgType, String data) {
        return sendMPCustomMessage(token, openid, msgType, data, null);
    }

    /**
     * 发送客服消息（小程序）
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param msgType 消息类型
     * @param data 消息内容
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendMPCustomMessage(String token, String openid, CustomMessageType msgType, String data, String kf_account) {
        return sendCustomMessage(AppType.MP, token, openid, msgType, data, kf_account);
    }


    /**
     * 发送客服文本消息
     * @param type 应用类型
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param content 文本内容
     * @return 发送结果
     */
    public static WxResult sendCustomTextMessage(AppType type, String token, String openid, String content) {
        return sendCustomTextMessage(type, token, openid, content, null);
    }

    /**
     * 发送客服文本消息
     * @param type 应用类型
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param content 文本内容
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendCustomTextMessage(AppType type, String token, String openid, String content, String kf_account) {
        String data = new JSONObject() {{ put("content", content); }}.toJSONString();
        return sendCustomMessage(type, token, openid, CustomMessageType.text, data, kf_account);
    }


    /**
     * 发送客服图片消息
     * @param type 应用类型
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param media_id 图片的媒体ID
     * @return 发送结果
     */
    public static WxResult sendCustomImageMessage(AppType type, String token, String openid, String media_id) {
        return sendCustomImageMessage(type, token, openid, media_id, null);
    }

    /**
     * 发送客服图片消息
     * @param type 应用类型
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param media_id 图片的媒体ID
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendCustomImageMessage(AppType type, String token, String openid, String media_id, String kf_account) {
        String data = new JSONObject() {{ put("media_id", media_id); }}.toJSONString();
        return sendCustomMessage(type, token, openid, CustomMessageType.image, data, kf_account);
    }


    /**
     * 发送客服消息
     * @param type 应用类型
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param msgType 消息类型
     * @param data 消息内容
     * @return 发送结果
     */
    public static WxResult sendCustomMessage(AppType type, String token, String openid, CustomMessageType msgType, String data) {
        return sendCustomMessage(type, token, openid, msgType, data, null);
    }

    /**
     * 发送客服消息
     * @param type 应用类型
     * @param token 接口调用凭据
     * @param openid 用户的唯一标识
     * @param msgType 消息类型
     * @param data 消息内容
     * @param kf_account 客服账号
     * @return 发送结果
     */
    public static WxResult sendCustomMessage(AppType type, String token, String openid, CustomMessageType msgType, String data, String kf_account) {
        String url = type == AppType.MP ? MP_SEND_CUSTOM_MESSAGE_URL : SEND_CUSTOM_MESSAGE_URL;
        String customservice = kf_account == null || kf_account.isEmpty() ? "" : ",\"customservice\":{\"kf_account\":\"" + kf_account + "\"}";
        String requestData = format("'{'\"touser\":\"{0}\",\"msgtype\":\"{1}\",\"{2}\":{3}{4}'}'", openid, msgType.name(), msgType.name(), data, customservice);
        String result = post(format(url, token), requestData, WECHAT_AGENT);
        return JSONObject.parseObject(result, WxResult.class);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="模板消息">

    /**
     * 发送模板消息（公众号）
     * @param token 接口调用凭据
     * @param data 消息内容
     * @return 发送结果
     */
    public static WxResult sendTemplateMessage(String token, String data) {
        String result = post(format(WEB_SEND_TEMPLATE_MESSAGE_URL, token), data, WECHAT_AGENT);
        return JSONObject.parseObject(result, WxResult.class);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="上传素材">

    /**
     * 上传临时缩略图素材（公众号）
     * @param token 接口调用凭据
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadThumbMedia(String token, String filePath) {
        return uploadThumbMedia(token, new File(filePath));
    }

    /**
     * 上传临时缩略图素材（公众号）
     * @param token 接口调用凭据
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadThumbMedia(String token, File file) {
        return uploadMedia(token, MediaType.thumb, file);
    }

    /**
     * 上传临时缩略图素材（小程序）
     * @param token 接口调用凭据
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadMPThumbMedia(String token, String filePath) {
        return uploadMPThumbMedia(token, new File(filePath));
    }

    /**
     * 上传临时缩略图素材（小程序）
     * @param token 接口调用凭据
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadMPThumbMedia(String token, File file) {
        return uploadMPMedia(token, MediaType.thumb, file);
    }

    /**
     * 上传临时视频素材（公众号）
     * @param token 接口调用凭据
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadVideoMedia(String token, String filePath) {
        return uploadVideoMedia(token, new File(filePath));
    }

    /**
     * 上传临时视频素材（公众号）
     * @param token 接口调用凭据
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadVideoMedia(String token, File file) {
        return uploadMedia(token, MediaType.video, file);
    }

    /**
     * 上传临时视频素材（小程序）
     * @param token 接口调用凭据
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadMPVideoMedia(String token, String filePath) {
        return uploadMPVideoMedia(token, new File(filePath));
    }

    /**
     * 上传临时视频素材（小程序）
     * @param token 接口调用凭据
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadMPVideoMedia(String token, File file) {
        return uploadMPMedia(token, MediaType.video, file);
    }

    /**
     * 上传临时语音素材（公众号）
     * @param token 接口调用凭据
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadVoiceMedia(String token, String filePath) {
        return uploadVoiceMedia(token, new File(filePath));
    }

    /**
     * 上传临时语音素材（公众号）
     * @param token 接口调用凭据
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadVoiceMedia(String token, File file) {
        return uploadMedia(token, MediaType.voice, file);
    }

    /**
     * 上传临时语音素材（小程序）
     * @param token 接口调用凭据
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadMPVoiceMedia(String token, String filePath) {
        return uploadMPVoiceMedia(token, new File(filePath));
    }

    /**
     * 上传临时语音素材（小程序）
     * @param token 接口调用凭据
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadMPVoiceMedia(String token, File file) {
        return uploadMPMedia(token, MediaType.voice, file);
    }

    /**
     * 上传临时图片素材（公众号）
     * @param token 接口调用凭据
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadImageMedia(String token, String filePath) {
        return uploadImageMedia(token, new File(filePath));
    }

    /**
     * 上传临时图片素材（公众号）
     * @param token 接口调用凭据
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadImageMedia(String token, File file) {
        return uploadImageMedia(AppType.WEB, token, file);
    }

    /**
     * 上传临时图片素材（小程序）
     * @param token 接口调用凭据
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadMPImageMedia(String token, String filePath) {
        return uploadMPImageMedia(token, new File(filePath));
    }

    /**
     * 上传临时图片素材（小程序）
     * @param token 接口调用凭据
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadMPImageMedia(String token, File file) {
        return uploadImageMedia(AppType.MP, token, file);
    }

    /**
     * 上传临时素材（公众号）
     * @param token 接口调用凭据
     * @param mediaType 媒体文件类型
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadMedia(String token, MediaType mediaType, String filePath) {
        return uploadMedia(token, mediaType, new File(filePath));
    }

    /**
     * 上传临时素材（公众号）
     * @param token 接口调用凭据
     * @param mediaType 媒体文件类型
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadMedia(String token, MediaType mediaType, File file) {
        return uploadMedia(AppType.WEB, token, mediaType, file);
    }

    /**
     * 上传临时素材（小程序）
     * @param token 接口调用凭据
     * @param mediaType 媒体文件类型
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadMPMedia(String token, MediaType mediaType, String filePath) {
        return uploadMPMedia(token, mediaType, new File(filePath));
    }

    /**
     * 上传临时素材（小程序）
     * @param token 接口调用凭据
     * @param mediaType 媒体文件类型
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadMPMedia(String token, MediaType mediaType, File file) {
        return uploadMedia(AppType.MP, token, mediaType, file);
    }

    /**
     * 上传临时素材
     * @param type 应用类型
     * @param token 接口调用凭据
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadImageMedia(AppType type, String token, String filePath) {
        return uploadImageMedia(type, token, new File(filePath));
    }

    /**
     * 上传临时图片素材
     * @param type 应用类型
     * @param token 接口调用凭据
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadImageMedia(AppType type, String token, File file) {
        return uploadMedia(type, token, MediaType.image, file);
    }

    /**
     * 上传临时素材
     * @param type 应用类型
     * @param token 接口调用凭据
     * @param mediaType 媒体文件类型
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadMedia(AppType type, String token, MediaType mediaType, String filePath) {
        return uploadMedia(type, token, mediaType, new File(filePath));
    }

    /**
     * 上传临时素材
     * @param type 应用类型
     * @param token 接口调用凭据
     * @param mediaType 媒体文件类型
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadMedia(AppType type, String token, MediaType mediaType, File file) {
        String url = type == AppType.MP ? MP_UPLOAD_MEDIA_URL : UPLOAD_MEDIA_URL;
        String result = request(format(url, token, mediaType.name()), file, WECHAT_AGENT);
        return JSONObject.parseObject(result, WxMediaResult.class);
    }


    /**
     * 上传永久缩略图素材（公众号）
     * @param token 接口调用凭据
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadThumbMaterial(String token, File file) {
        return uploadMaterial(token, MediaType.thumb, file);
    }

    /**
     * 上传永久视频素材（公众号）
     * @param token 接口调用凭据
     * @param file 文件
     * @param title 标题
     * @param introduction 描述
     * @return 上传结果
     */
    public static WxMediaResult uploadVideoMaterial(String token, File file, String title, String introduction) {
        return uploadMaterial(token, MediaType.video, file, new HashMap<String, String>() {{
            put("description", new JSONObject() {{ put("title", title); put("introduction", introduction); }}.toJSONString());
        }});
    }

    /**
     * 上传永久语音素材（公众号）
     * @param token 接口调用凭据
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadVoiceMaterial(String token, File file) {
        return uploadMaterial(token, MediaType.voice, file);
    }

    /**
     * 上传永久图片素材（公众号）
     * @param token 接口调用凭据
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadImageMaterial(String token, File file) {
        return uploadMaterial(token, MediaType.image, file);
    }

    /**
     * 上传永久素材（公众号）
     * @param token 接口调用凭据
     * @param mediaType 媒体文件类型
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadMaterial(String token, MediaType mediaType, File file) {
        return uploadMaterial(token, mediaType, file, null);
    }

    /**
     * 上传永久素材（公众号）
     * @param token 接口调用凭据
     * @param mediaType 媒体文件类型
     * @param file 文件
     * @param data 附加数据
     * @return 上传结果
     */
    public static WxMediaResult uploadMaterial(String token, MediaType mediaType, File file, Map<String, String> data) {
        String result = request(format(UPLOAD_MATERIAL_URL, token, mediaType.name()), file, data, WECHAT_AGENT);
        return JSONObject.parseObject(result, WxMediaResult.class);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="客服消息（企业微信）">

    /**
     * 读取企业微信客服消息
     * @param accessToken 接口调用凭据
     * @param cursor 上一次调用时返回的next_cursor，第一次拉取可以不填。若不填，从3天内最早的消息开始返回。不多于64字节
     * @param token 回调事件返回的token字段，10分钟内有效；可不填，如果不填接口有严格的频率限制。不多于128字节
     * @param open_kfid 指定拉取某个客服帐号的消息，否则默认返回有权限的客服帐号的消息。当客服帐号较多，建议按open_kfid来拉取以获取更好的性能。
     * @param voice_format 语音消息类型，0-Amr 1-Silk，默认0。可通过该参数控制返回的语音格式，开发者可按需选择自己程序支持的一种格式
     * @param limit 期望请求的数据量，默认值和最大值都为1000。注意：可能会出现返回条数少于limit的情况，需结合返回的has_more字段判断是否继续请求。
     * @return 消息列表
     */
    public static WxKfMsg getWxKfSyncMsg(String accessToken, String cursor, String token, String open_kfid, int voice_format, int limit) {
        String data = new JSONObject() {{
            put("cursor", cursor);
            put("token", token);
            put("open_kfid", open_kfid);
            put("voice_format", voice_format);
            put("limit", limit);
        }}.toJSONString();
        String result = post(format(WX_KF_SYNC_MSG_URL, accessToken), data, WECHAT_AGENT);
        return JSONObject.parseObject(result, WxKfMsg.class);
    }


    /**
     * 发送企业微信客服菜单消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param head_content 起始文本，不多于1024字节
     * @param tail_content 结束文本，不多于1024字节
     * @param list 菜单项配置，不超过50个，其中click/view/miniprogram的菜单类型加起来不超过10个
     * @return 发送结果
     */
    public static WxKfResult sendWxKfMsgmenuMsg(String token, String touser, String open_kfid, String head_content, String tail_content, List<WxKfMsgMenuSubitem> list) {
        return sendWxKfMsgmenuMsg(token, touser, open_kfid, head_content, tail_content, list, null);
    }

    /**
     * 发送企业微信客服菜单消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param head_content 起始文本，不多于1024字节
     * @param tail_content 结束文本，不多于1024字节
     * @param list 菜单项配置，不超过50个，其中click/view/miniprogram的菜单类型加起来不超过10个
     * @param msgid 指定消息ID
     * @return 发送结果
     */
    public static WxKfResult sendWxKfMsgmenuMsg(String token, String touser, String open_kfid, String head_content, String tail_content, List<WxKfMsgMenuSubitem> list, String msgid) {
        return sendWxKfMsgmenuMsg(token, touser, open_kfid, head_content, tail_content, msgid, list.toArray(new WxKfMsgMenuSubitem[list.size()]));
    }

    /**
     * 发送企业微信客服菜单消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param head_content 起始文本，不多于1024字节
     * @param tail_content 结束文本，不多于1024字节
     * @param items 菜单项配置，不超过50个，其中click/view/miniprogram的菜单类型加起来不超过10个
     * @return 发送结果
     */
    public static WxKfResult sendWxKfMsgmenuMsg(String token, String touser, String open_kfid, String head_content, String tail_content, WxKfMsgMenuSubitem ...items) {
        return sendWxKfMsgmenuMsg(token, touser, open_kfid, head_content, tail_content, null, items);
    }

    /**
     * 发送企业微信客服菜单消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param head_content 起始文本，不多于1024字节
     * @param tail_content 结束文本，不多于1024字节
     * @param msgid 指定消息ID
     * @param items 菜单项配置，不超过50个，其中click/view/miniprogram的菜单类型加起来不超过10个
     * @return 发送结果
     */
    public static WxKfResult sendWxKfMsgmenuMsg(String token, String touser, String open_kfid, String head_content, String tail_content, String msgid, WxKfMsgMenuSubitem ...items) {
        JSONArray list = new JSONArray();
        for (WxKfMsgMenuSubitem item : items) {
            if (item.getType().equals("view")) list.add(JsonUtils.removeObjectKeysWithout(item, "type", "view"));
            else if (item.getType().equals("text")) list.add(JsonUtils.removeObjectKeysWithout(item, "type", "text"));
            else if (item.getType().equals("click")) list.add(JsonUtils.removeObjectKeysWithout(item, "type", "click"));
            else if (item.getType().equals("miniprogram")) list.add(JsonUtils.removeObjectKeysWithout(item, "type", "miniprogram"));
        }
        String data = new JSONObject() {{
            put("head_content", head_content);
            put("tail_content", tail_content);
            put("list", list);
        }}.toJSONString();
        return sendWxKfMsg(token, touser, open_kfid, CustomMessageType.msgmenu, data, msgid);
    }


    /**
     * 发送企业微信客服地理位置消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param name 位置名
     * @param address 地址详情说明
     * @param latitude 纬度，浮点数，范围为90 ~ -90
     * @param longitude 经度，浮点数，范围为180 ~ -180
     * @return 发送结果
     */
    public static WxKfResult sendWxKfLocationMsg(String token, String touser, String open_kfid, String name, String address, double latitude, double longitude) {
        return sendWxKfLocationMsg(token, touser, open_kfid, name, address, latitude, longitude, null);
    }

    /**
     * 发送企业微信客服地理位置消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param name 位置名
     * @param address 地址详情说明
     * @param latitude 纬度，浮点数，范围为90 ~ -90
     * @param longitude 经度，浮点数，范围为180 ~ -180
     * @param msgid 指定消息ID
     * @return 发送结果
     */
    public static WxKfResult sendWxKfLocationMsg(String token, String touser, String open_kfid, String name, String address, double latitude, double longitude, String msgid) {
        String data = new JSONObject() {{
            put("name", name);
            put("address", address);
            put("latitude", latitude);
            put("longitude", longitude);
        }}.toJSONString();
        return sendWxKfMsg(token, touser, open_kfid, CustomMessageType.location, data, msgid);
    }


    /**
     * 发送企业微信客服小程序消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param appid 小程序appid
     * @param title 小程序消息标题，最多64个字节，超过会自动截断
     * @param pagepath 点击消息卡片后进入的小程序页面路径。注意路径要以.html为后缀，否则在微信中打开会提示找不到页面
     * @param thumb_media_id 小程序消息封面的mediaid，封面图建议尺寸为520*416
     * @return 发送结果
     */
    public static WxKfResult sendWxKfMiniprogramMsg(String token, String touser, String open_kfid, String appid, String title, String pagepath, String thumb_media_id) {
        return sendWxKfMiniprogramMsg(token, touser, open_kfid, appid, title, pagepath, thumb_media_id, null);
    }

    /**
     * 发送企业微信客服小程序消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param appid 小程序appid
     * @param title 小程序消息标题，最多64个字节，超过会自动截断
     * @param pagepath 点击消息卡片后进入的小程序页面路径。注意路径要以.html为后缀，否则在微信中打开会提示找不到页面
     * @param thumb_media_id 小程序消息封面的mediaid，封面图建议尺寸为520*416
     * @param msgid 指定消息ID
     * @return 发送结果
     */
    public static WxKfResult sendWxKfMiniprogramMsg(String token, String touser, String open_kfid, String appid, String title, String pagepath, String thumb_media_id, String msgid) {
        String data = new JSONObject() {{
            put("appid", appid);
            put("title", title);
            put("pagepath", pagepath);
            put("thumb_media_id", thumb_media_id);
        }}.toJSONString();
        return sendWxKfMsg(token, touser, open_kfid, CustomMessageType.miniprogram, data, msgid);
    }


    /**
     * 发送企业微信客服图文链接消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param title 标题，不超过128个字节，超过会自动截断
     * @param desc 描述，不超过512个字节，超过会自动截断
     * @param url 点击后跳转的链接。 最长2048字节，请确保包含了协议头(http/https)
     * @param thumb_media_id 缩略图的media_id, 可以通过素材管理接口获得。此处thumb_media_id即上传接口返回的media_id
     * @return 发送结果
     */
    public static WxKfResult sendWxKfLinkMsg(String token, String touser, String open_kfid, String title, String desc, String url, String thumb_media_id) {
        return sendWxKfLinkMsg(token, touser, open_kfid, title, desc, url, thumb_media_id, null);
    }

    /**
     * 发送企业微信客服图文链接消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param title 标题，不超过128个字节，超过会自动截断
     * @param desc 描述，不超过512个字节，超过会自动截断
     * @param url 点击后跳转的链接。 最长2048字节，请确保包含了协议头(http/https)
     * @param thumb_media_id 缩略图的media_id, 可以通过素材管理接口获得。此处thumb_media_id即上传接口返回的media_id
     * @param msgid 指定消息ID
     * @return 发送结果
     */
    public static WxKfResult sendWxKfLinkMsg(String token, String touser, String open_kfid, String title, String desc, String url, String thumb_media_id, String msgid) {
        String data = new JSONObject() {{
            put("title", title);
            put("desc", desc);
            put("url", url);
            put("thumb_media_id", thumb_media_id);
        }}.toJSONString();
        return sendWxKfMsg(token, touser, open_kfid, CustomMessageType.link, data, msgid);
    }


    /**
     * 发送企业微信客服文件消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param media_id 文件id，可以调用上传临时素材接口获取
     * @return 发送结果
     */
    public static WxKfResult sendWxKfFileMsg(String token, String touser, String open_kfid, String media_id) {
        return sendWxKfFileMsg(token, touser, open_kfid, media_id, null);
    }

    /**
     * 发送企业微信客服文件消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param media_id 文件id，可以调用上传临时素材接口获取
     * @param msgid 指定消息ID
     * @return 发送结果
     */
    public static WxKfResult sendWxKfFileMsg(String token, String touser, String open_kfid, String media_id, String msgid) {
        String data = new JSONObject() {{ put("media_id", media_id); }}.toJSONString();
        return sendWxKfMsg(token, touser, open_kfid, CustomMessageType.file, data, msgid);
    }


    /**
     * 发送企业微信客服视频消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param media_id 视频媒体文件id，可以调用上传临时素材接口获取
     * @return 发送结果
     */
    public static WxKfResult sendWxKfVideoMsg(String token, String touser, String open_kfid, String media_id) {
        return sendWxKfVideoMsg(token, touser, open_kfid, media_id, null);
    }

    /**
     * 发送企业微信客服视频消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param media_id 视频媒体文件id，可以调用上传临时素材接口获取
     * @param msgid 指定消息ID
     * @return 发送结果
     */
    public static WxKfResult sendWxKfVideoMsg(String token, String touser, String open_kfid, String media_id, String msgid) {
        String data = new JSONObject() {{ put("media_id", media_id); }}.toJSONString();
        return sendWxKfMsg(token, touser, open_kfid, CustomMessageType.video, data, msgid);
    }


    /**
     * 发送企业微信客服语音消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param media_id 语音文件id，可以调用上传临时素材接口获取
     * @return 发送结果
     */
    public static WxKfResult sendWxKfVoiceMsg(String token, String touser, String open_kfid, String media_id) {
        return sendWxKfVoiceMsg(token, touser, open_kfid, media_id, null);
    }

    /**
     * 发送企业微信客服语音消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param media_id 语音文件id，可以调用上传临时素材接口获取
     * @param msgid 指定消息ID
     * @return 发送结果
     */
    public static WxKfResult sendWxKfVoiceMsg(String token, String touser, String open_kfid, String media_id, String msgid) {
        String data = new JSONObject() {{ put("media_id", media_id); }}.toJSONString();
        return sendWxKfMsg(token, touser, open_kfid, CustomMessageType.voice, data, msgid);
    }


    /**
     * 发送企业微信客服图片消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param media_id 图片文件id，可以调用上传临时素材接口获取
     * @return 发送结果
     */
    public static WxKfResult sendWxKfImageMsg(String token, String touser, String open_kfid, String media_id) {
        return sendWxKfImageMsg(token, touser, open_kfid, media_id, null);
    }

    /**
     * 发送企业微信客服图片消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param media_id 图片文件id，可以调用上传临时素材接口获取
     * @param msgid 指定消息ID
     * @return 发送结果
     */
    public static WxKfResult sendWxKfImageMsg(String token, String touser, String open_kfid, String media_id, String msgid) {
        String data = new JSONObject() {{ put("media_id", media_id); }}.toJSONString();
        return sendWxKfMsg(token, touser, open_kfid, CustomMessageType.image, data, msgid);
    }


    /**
     * 发送企业微信客服文本消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param content 文本内容
     * @return 发送结果
     */
    public static WxKfResult sendWxKfTextMsg(String token, String touser, String open_kfid, String content) {
        return sendWxKfTextMsg(token, touser, open_kfid, content, null);
    }

    /**
     * 发送企业微信客服文本消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param content 文本内容
     * @param msgid 指定消息ID
     * @return 发送结果
     */
    public static WxKfResult sendWxKfTextMsg(String token, String touser, String open_kfid, String content, String msgid) {
        String data = new JSONObject() {{ put("content", content); }}.toJSONString();
        return sendWxKfMsg(token, touser, open_kfid, CustomMessageType.text, data, msgid);
    }


    /**
     * 发送企业微信客服消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param msgType 消息类型
     * @param data 消息内容
     * @return 发送结果
     */
    public static WxKfResult sendWxKfMsg(String token, String touser, String open_kfid, CustomMessageType msgType, String data) {
        return sendWxKfMsg(token, touser, open_kfid, msgType, data, null);
    }

    /**
     * 发送企业微信客服消息
     * @param token 接口调用凭据
     * @param touser 指定接收消息的客户UserID
     * @param open_kfid 指定发送消息的客服帐号ID
     * @param msgType 消息类型
     * @param data 消息内容
     * @param msgid 指定消息ID
     * @return 发送结果
     */
    public static WxKfResult sendWxKfMsg(String token, String touser, String open_kfid, CustomMessageType msgType, String data, String msgid) {
        String msgdata = msgid == null || msgid.isEmpty() ? "" : ",\"msgid\":\"" + msgid + "\"";
        String requestData = format("'{'\"touser\":\"{0}\",\"open_kfid\":\"{1}\",\"msgtype\":\"{2}\",\"{3}\":{4}{5}'}'", touser, open_kfid, msgType.name(), msgType.name(), data, msgdata);
        String result = post(format(WX_KF_SEND_MSG_URL, token), requestData, WECHAT_AGENT);
        return JSONObject.parseObject(result, WxKfResult.class);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="特定事件客服消息（企业微信）">

    /**
     * 发送企业微信特定事件客服菜单消息
     * @param token 接口调用凭据
     * @param code 事件响应消息对应的code。通过事件回调下发，仅可使用一次。
     * @param head_content 起始文本，不多于1024字节
     * @param tail_content 结束文本，不多于1024字节
     * @param list 菜单项配置，不超过50个，其中click/view/miniprogram的菜单类型加起来不超过10个
     * @return 发送结果
     */
    public static WxKfResult sendWxKfMsgmenuMsgOnEvent(String token, String code, String head_content, String tail_content, List<WxKfMsgMenuSubitem> list) {
        return sendWxKfMsgmenuMsgOnEvent(token, code, head_content, tail_content, list, null);
    }

    /**
     * 发送企业微信特定事件客服菜单消息
     * @param token 接口调用凭据
     * @param code 事件响应消息对应的code。通过事件回调下发，仅可使用一次。
     * @param head_content 起始文本，不多于1024字节
     * @param tail_content 结束文本，不多于1024字节
     * @param list 菜单项配置，不超过50个，其中click/view/miniprogram的菜单类型加起来不超过10个
     * @param msgid 指定消息ID
     * @return 发送结果
     */
    public static WxKfResult sendWxKfMsgmenuMsgOnEvent(String token, String code, String head_content, String tail_content, List<WxKfMsgMenuSubitem> list, String msgid) {
        return sendWxKfMsgmenuMsgOnEvent(token, code, head_content, tail_content, msgid, list.toArray(new WxKfMsgMenuSubitem[list.size()]));
    }

    /**
     * 发送企业微信特定事件客服菜单消息
     * @param token 接口调用凭据
     * @param code 事件响应消息对应的code。通过事件回调下发，仅可使用一次。
     * @param head_content 起始文本，不多于1024字节
     * @param tail_content 结束文本，不多于1024字节
     * @param items 菜单项配置，不超过50个，其中click/view/miniprogram的菜单类型加起来不超过10个
     * @return 发送结果
     */
    public static WxKfResult sendWxKfMsgmenuMsgOnEvent(String token, String code, String head_content, String tail_content, WxKfMsgMenuSubitem ...items) {
        return sendWxKfMsgmenuMsgOnEvent(token, code, head_content, tail_content, null, items);
    }

    /**
     * 发送企业微信特定事件客服菜单消息
     * @param token 接口调用凭据
     * @param code 事件响应消息对应的code。通过事件回调下发，仅可使用一次。
     * @param head_content 起始文本，不多于1024字节
     * @param tail_content 结束文本，不多于1024字节
     * @param msgid 指定消息ID
     * @param items 菜单项配置，不超过50个，其中click/view/miniprogram的菜单类型加起来不超过10个
     * @return 发送结果
     */
    public static WxKfResult sendWxKfMsgmenuMsgOnEvent(String token, String code, String head_content, String tail_content, String msgid, WxKfMsgMenuSubitem ...items) {
        JSONArray list = new JSONArray();
        for (WxKfMsgMenuSubitem item : items) {
            if (item.getType().equals("view")) list.add(JsonUtils.removeObjectKeysWithout(item, "type", "view"));
            else if (item.getType().equals("text")) list.add(JsonUtils.removeObjectKeysWithout(item, "type", "text"));
            else if (item.getType().equals("click")) list.add(JsonUtils.removeObjectKeysWithout(item, "type", "click"));
            else if (item.getType().equals("miniprogram")) list.add(JsonUtils.removeObjectKeysWithout(item, "type", "miniprogram"));
        }
        String data = new JSONObject() {{
            put("head_content", head_content);
            put("tail_content", tail_content);
            put("list", list);
        }}.toJSONString();
        return sendWxKfMsgOnEvent(token, code, CustomMessageType.msgmenu, data, msgid);
    }


    /**
     * 发送企业微信特定事件客服文本消息
     * @param token 接口调用凭据
     * @param code 事件响应消息对应的code。通过事件回调下发，仅可使用一次。
     * @param content 文本内容
     * @return 发送结果
     */
    public static WxKfResult sendWxKfTextMsgOnEvent(String token, String code, String content) {
        return sendWxKfTextMsgOnEvent(token, code, content, null);
    }

    /**
     * 发送企业微信特定事件客服文本消息
     * @param token 接口调用凭据
     * @param code 事件响应消息对应的code。通过事件回调下发，仅可使用一次。
     * @param content 文本内容
     * @param msgid 指定消息ID
     * @return 发送结果
     */
    public static WxKfResult sendWxKfTextMsgOnEvent(String token, String code, String content, String msgid) {
        String data = new JSONObject() {{ put("content", content); }}.toJSONString();
        return sendWxKfMsgOnEvent(token, code, CustomMessageType.text, data, msgid);
    }


    /**
     * 发送企业微信特定事件客服消息
     * @param token 接口调用凭据
     * @param code 事件响应消息对应的code。通过事件回调下发，仅可使用一次。
     * @param msgType 消息类型
     * @param data 消息内容
     * @return 发送结果
     */
    public static WxKfResult sendWxKfMsgOnEvent(String token, String code, CustomMessageType msgType, String data) {
        return sendWxKfMsgOnEvent(token, code, msgType, data, null);
    }

    /**
     * 发送企业微信特定事件客服消息
     * @param token 接口调用凭据
     * @param code 事件响应消息对应的code。通过事件回调下发，仅可使用一次。
     * @param msgType 消息类型
     * @param data 消息内容
     * @param msgid 消息ID。如果请求参数指定了msgid，则原样返回，否则系统自动生成并返回。不多于32字节，字符串取值范围(正则表达式)：[0-9a-zA-Z_-]*
     * @return 发送结果
     */
    public static WxKfResult sendWxKfMsgOnEvent(String token, String code, CustomMessageType msgType, String data, String msgid) {
        String msgdata = msgid == null || msgid.isEmpty() ? "" : ",\"msgid\":\"" + msgid + "\"";
        String requestData = format("'{'\"code\":\"{0}\",\"msgtype\":\"{1}\",\"{2}\":{3}{4}'}'", code, msgType.name(), msgType.name(), data, msgdata);
        String result = post(format(WX_KF_SEND_MSG_ON_EVENT_URL, token), requestData, WECHAT_AGENT);
        return JSONObject.parseObject(result, WxKfResult.class);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="上传素材（企业微信）">

    /**
     * 上传临时文件素材（企业微信）
     * @param token 接口调用凭据
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadWxFileMedia(String token, String filePath) {
        return uploadWxFileMedia(token, new File(filePath));
    }

    /**
     * 上传临时文件素材（企业微信）
     * @param token 接口调用凭据
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadWxFileMedia(String token, File file) {
        return uploadWxMedia(token, MediaType.file, file);
    }

    /**
     * 上传临时视频素材（企业微信）
     * @param token 接口调用凭据
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadWxVideoMedia(String token, String filePath) {
        return uploadWxVideoMedia(token, new File(filePath));
    }

    /**
     * 上传临时视频素材（企业微信）
     * @param token 接口调用凭据
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadWxVideoMedia(String token, File file) {
        return uploadWxMedia(token, MediaType.video, file);
    }

    /**
     * 上传临时语音素材（企业微信）
     * @param token 接口调用凭据
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadWxVoiceMedia(String token, String filePath) {
        return uploadWxVoiceMedia(token, new File(filePath));
    }

    /**
     * 上传临时语音素材（企业微信）
     * @param token 接口调用凭据
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadWxVoiceMedia(String token, File file) {
        return uploadWxMedia(token, MediaType.voice, file);
    }

    /**
     * 上传临时图片素材（企业微信）
     * @param token 接口调用凭据
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadWxImageMedia(String token, String filePath) {
        return uploadWxImageMedia(token, new File(filePath));
    }

    /**
     * 上传临时图片素材（企业微信）
     * @param token 接口调用凭据
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadWxImageMedia(String token, File file) {
        return uploadWxMedia(token, MediaType.image, file);
    }

    /**
     * 上传临时素材（企业微信）
     * @param token 接口调用凭据
     * @param mediaType 媒体文件类型
     * @param filePath 文件路径
     * @return 上传结果
     */
    public static WxMediaResult uploadWxMedia(String token, MediaType mediaType, String filePath) {
        return uploadWxMedia(token, mediaType, new File(filePath));
    }

    /**
     * 上传临时素材（企业微信）
     * @param token 接口调用凭据
     * @param mediaType 媒体文件类型
     * @param file 文件
     * @return 上传结果
     */
    public static WxMediaResult uploadWxMedia(String token, MediaType mediaType, File file) {
        String result = request(format(WX_UPLOAD_MEDIA_URL, token, mediaType.name()), file, WECHAT_AGENT);
        return JSONObject.parseObject(result, WxMediaResult.class);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="小程序码">

    /**
     * 获取小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量较少的业务场景。通过该接口生成的小程序码，永久有效，有数量限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param path 扫码进入的小程序页面路径，最大长度 1024 个字符，不能为空，scancode_time为系统保留参数，不允许配置；对于小游戏，可以只传入 query 部分，来实现传参效果，如：传入 "?foo=bar"，即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}
     * @return 小程序码
     */
    public static WxBufferResult getWxaCode(String token, String path) {
        return getWxaCode(token, path, 0);
    }

    /**
     * 获取小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量较少的业务场景。通过该接口生成的小程序码，永久有效，有数量限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param path 扫码进入的小程序页面路径，最大长度 1024 个字符，不能为空，scancode_time为系统保留参数，不允许配置；对于小游戏，可以只传入 query 部分，来实现传参效果，如：传入 "?foo=bar"，即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}
     * @param colorR auto_color 为 false 时生效，颜色R值，默认 0
     * @param colorG auto_color 为 false 时生效，颜色G值，默认 0
     * @param colorB auto_color 为 false 时生效，颜色B值，默认 0
     * @return 小程序码
     */
    public static WxBufferResult getWxaCode(String token, String path, int colorR, int colorG, int colorB) {
        return getWxaCode(token, path, 0, colorR, colorG, colorB);
    }

    /**
     * 获取小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量较少的业务场景。通过该接口生成的小程序码，永久有效，有数量限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param path 扫码进入的小程序页面路径，最大长度 1024 个字符，不能为空，scancode_time为系统保留参数，不允许配置；对于小游戏，可以只传入 query 部分，来实现传参效果，如：传入 "?foo=bar"，即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}
     * @param width 二维码的宽度，单位 px。默认值为430，最小 280px，最大 1280px
     * @return 小程序码
     */
    public static WxBufferResult getWxaCode(String token, String path, int width) {
        return getWxaCode(token, path, width, false);
    }

    /**
     * 获取小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量较少的业务场景。通过该接口生成的小程序码，永久有效，有数量限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param path 扫码进入的小程序页面路径，最大长度 1024 个字符，不能为空，scancode_time为系统保留参数，不允许配置；对于小游戏，可以只传入 query 部分，来实现传参效果，如：传入 "?foo=bar"，即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}
     * @param width 二维码的宽度，单位 px。默认值为430，最小 280px，最大 1280px
     * @param colorR auto_color 为 false 时生效，颜色R值，默认 0
     * @param colorG auto_color 为 false 时生效，颜色G值，默认 0
     * @param colorB auto_color 为 false 时生效，颜色B值，默认 0
     * @return 小程序码
     */
    public static WxBufferResult getWxaCode(String token, String path, int width, int colorR, int colorG, int colorB) {
        return getWxaCode(token, path, width, colorR, colorG, colorB, false);
    }

    /**
     * 获取小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量较少的业务场景。通过该接口生成的小程序码，永久有效，有数量限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param path 扫码进入的小程序页面路径，最大长度 1024 个字符，不能为空，scancode_time为系统保留参数，不允许配置；对于小游戏，可以只传入 query 部分，来实现传参效果，如：传入 "?foo=bar"，即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}
     * @param width 二维码的宽度，单位 px。默认值为430，最小 280px，最大 1280px
     * @param isHyaline 默认值false；是否需要透明底色，为 true 时，生成透明底色的小程序码
     * @return 小程序码
     */
    public static WxBufferResult getWxaCode(String token, String path, int width, boolean isHyaline) {
        return getWxaCode(token, path, width, isHyaline, null);
    }

    /**
     * 获取小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量较少的业务场景。通过该接口生成的小程序码，永久有效，有数量限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param path 扫码进入的小程序页面路径，最大长度 1024 个字符，不能为空，scancode_time为系统保留参数，不允许配置；对于小游戏，可以只传入 query 部分，来实现传参效果，如：传入 "?foo=bar"，即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}
     * @param width 二维码的宽度，单位 px。默认值为430，最小 280px，最大 1280px
     * @param colorR auto_color 为 false 时生效，颜色R值，默认 0
     * @param colorG auto_color 为 false 时生效，颜色G值，默认 0
     * @param colorB auto_color 为 false 时生效，颜色B值，默认 0
     * @param isHyaline 默认值false；是否需要透明底色，为 true 时，生成透明底色的小程序码
     * @return 小程序码
     */
    public static WxBufferResult getWxaCode(String token, String path, int width, int colorR, int colorG, int colorB, boolean isHyaline) {
        return getWxaCode(token, path, width, colorR, colorG, colorB, isHyaline, null);
    }

    /**
     * 获取小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量较少的业务场景。通过该接口生成的小程序码，永久有效，有数量限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param path 扫码进入的小程序页面路径，最大长度 1024 个字符，不能为空，scancode_time为系统保留参数，不允许配置；对于小游戏，可以只传入 query 部分，来实现传参效果，如：传入 "?foo=bar"，即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}
     * @param width 二维码的宽度，单位 px。默认值为430，最小 280px，最大 1280px
     * @param isHyaline 默认值false；是否需要透明底色，为 true 时，生成透明底色的小程序码
     * @param envVersion 要打开的小程序版本。正式版为 "release"，体验版为 "trial"，开发版为 "develop"。默认是正式版
     * @return 小程序码
     */
    public static WxBufferResult getWxaCode(String token, String path, int width, boolean isHyaline, MpVersion envVersion) {
        return getWxaCode(token, path, width, true, 0, 0, 0, isHyaline, envVersion);
    }

    /**
     * 获取小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量较少的业务场景。通过该接口生成的小程序码，永久有效，有数量限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param path 扫码进入的小程序页面路径，最大长度 1024 个字符，不能为空，scancode_time为系统保留参数，不允许配置；对于小游戏，可以只传入 query 部分，来实现传参效果，如：传入 "?foo=bar"，即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}
     * @param width 二维码的宽度，单位 px。默认值为430，最小 280px，最大 1280px
     * @param colorR auto_color 为 false 时生效，颜色R值，默认 0
     * @param colorG auto_color 为 false 时生效，颜色G值，默认 0
     * @param colorB auto_color 为 false 时生效，颜色B值，默认 0
     * @param isHyaline 默认值false；是否需要透明底色，为 true 时，生成透明底色的小程序码
     * @param envVersion 要打开的小程序版本。正式版为 "release"，体验版为 "trial"，开发版为 "develop"。默认是正式版
     * @return 小程序码
     */
    public static WxBufferResult getWxaCode(String token, String path, int width, int colorR, int colorG, int colorB, boolean isHyaline, MpVersion envVersion) {
        return getWxaCode(token, path, width, false, colorR, colorG, colorB, isHyaline, envVersion);
    }

    /**
     * 获取小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量较少的业务场景。通过该接口生成的小程序码，永久有效，有数量限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param path 扫码进入的小程序页面路径，最大长度 1024 个字符，不能为空，scancode_time为系统保留参数，不允许配置；对于小游戏，可以只传入 query 部分，来实现传参效果，如：传入 "?foo=bar"，即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}
     * @param width 二维码的宽度，单位 px。默认值为430，最小 280px，最大 1280px
     * @param autoColor 默认值false；自动配置线条颜色，如果颜色依然是黑色，则说明不建议配置主色调
     * @param colorR auto_color 为 false 时生效，颜色R值，默认 0
     * @param colorG auto_color 为 false 时生效，颜色G值，默认 0
     * @param colorB auto_color 为 false 时生效，颜色B值，默认 0
     * @param isHyaline 默认值false；是否需要透明底色，为 true 时，生成透明底色的小程序码
     * @param envVersion 要打开的小程序版本。正式版为 "release"，体验版为 "trial"，开发版为 "develop"。默认是正式版
     * @return 小程序码
     */
    public static WxBufferResult getWxaCode(String token, String path, int width, boolean autoColor, int colorR, int colorG, int colorB, boolean isHyaline, MpVersion envVersion) {
        JSONObject requestData = new JSONObject();
        requestData.put("path", path);
        if (width > 0) requestData.put("width", width);
        requestData.put("auto_color", autoColor);
        if (!autoColor) {
            JSONObject lineColor = new JSONObject();
            lineColor.put("r", colorR > 0 ? colorR : 0);
            lineColor.put("g", colorG > 0 ? colorG : 0);
            lineColor.put("b", colorB > 0 ? colorB : 0);
            requestData.put("line_color", lineColor);
        }
        requestData.put("is_hyaline", isHyaline);
        if (envVersion != null) requestData.put("env_version", envVersion.name());
        byte[] result = postByte(format(GET_WXA_CODE_URL, token), requestData.toJSONString(), WECHAT_AGENT);
        try {
            return JSONObject.parseObject(getText(result), WxBufferResult.class);
        } catch (Exception e) {
            WxBufferResult wxBufferResult = new WxBufferResult(0, "");
            wxBufferResult.setBuffer(result);
            return wxBufferResult;
        }
    }


    /**
     * 获取不限制的小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param scene 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符，其它字符请自行编码为合法字符（因不支持%，中文无法使用 urlencode 处理，请使用其他编码方式）
     * @return 不限制的小程序码
     */
    public static WxBufferResult getUnlimitWxaCode(String token, String scene) {
        return getUnlimitWxaCode(token, scene, null);
    }

    /**
     * 获取不限制的小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param scene 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符，其它字符请自行编码为合法字符（因不支持%，中文无法使用 urlencode 处理，请使用其他编码方式）
     * @param colorR auto_color 为 false 时生效，颜色R值，默认 0
     * @param colorG auto_color 为 false 时生效，颜色G值，默认 0
     * @param colorB auto_color 为 false 时生效，颜色B值，默认 0
     * @return 不限制的小程序码
     */
    public static WxBufferResult getUnlimitWxaCode(String token, String scene, int colorR, int colorG, int colorB) {
        return getUnlimitWxaCode(token, scene, null, colorR, colorG, colorB);
    }

    /**
     * 获取不限制的小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param scene 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符，其它字符请自行编码为合法字符（因不支持%，中文无法使用 urlencode 处理，请使用其他编码方式）
     * @param page 默认是主页，页面 page，例如 pages/index/index，根路径前不要填加 /，不能携带参数（参数请放在scene字段里），如果不填写这个字段，默认跳主页面。scancode_time为系统保留参数，不允许配置
     * @return 不限制的小程序码
     */
    public static WxBufferResult getUnlimitWxaCode(String token, String scene, String page) {
        return getUnlimitWxaCode(token, scene, page, true);
    }

    /**
     * 获取不限制的小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param scene 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符，其它字符请自行编码为合法字符（因不支持%，中文无法使用 urlencode 处理，请使用其他编码方式）
     * @param page 默认是主页，页面 page，例如 pages/index/index，根路径前不要填加 /，不能携带参数（参数请放在scene字段里），如果不填写这个字段，默认跳主页面。scancode_time为系统保留参数，不允许配置
     * @param colorR auto_color 为 false 时生效，颜色R值，默认 0
     * @param colorG auto_color 为 false 时生效，颜色G值，默认 0
     * @param colorB auto_color 为 false 时生效，颜色B值，默认 0
     * @return 不限制的小程序码
     */
    public static WxBufferResult getUnlimitWxaCode(String token, String scene, String page, int colorR, int colorG, int colorB) {
        return getUnlimitWxaCode(token, scene, page, true, colorR, colorG, colorB);
    }

    /**
     * 获取不限制的小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param scene 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符，其它字符请自行编码为合法字符（因不支持%，中文无法使用 urlencode 处理，请使用其他编码方式）
     * @param page 默认是主页，页面 page，例如 pages/index/index，根路径前不要填加 /，不能携带参数（参数请放在scene字段里），如果不填写这个字段，默认跳主页面。scancode_time为系统保留参数，不允许配置
     * @param check_path 默认是true，检查page 是否存在，为 true 时 page 必须是已经发布的小程序存在的页面（否则报错）；为 false 时允许小程序未发布或者 page 不存在， 但page 有数量上限（60000个）请勿滥用
     * @return 不限制的小程序码
     */
    public static WxBufferResult getUnlimitWxaCode(String token, String scene, String page, Boolean check_path) {
        return getUnlimitWxaCode(token, scene, page, check_path, 0);
    }

    /**
     * 获取不限制的小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param scene 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符，其它字符请自行编码为合法字符（因不支持%，中文无法使用 urlencode 处理，请使用其他编码方式）
     * @param page 默认是主页，页面 page，例如 pages/index/index，根路径前不要填加 /，不能携带参数（参数请放在scene字段里），如果不填写这个字段，默认跳主页面。scancode_time为系统保留参数，不允许配置
     * @param check_path 默认是true，检查page 是否存在，为 true 时 page 必须是已经发布的小程序存在的页面（否则报错）；为 false 时允许小程序未发布或者 page 不存在， 但page 有数量上限（60000个）请勿滥用
     * @param colorR auto_color 为 false 时生效，颜色R值，默认 0
     * @param colorG auto_color 为 false 时生效，颜色G值，默认 0
     * @param colorB auto_color 为 false 时生效，颜色B值，默认 0
     * @return 不限制的小程序码
     */
    public static WxBufferResult getUnlimitWxaCode(String token, String scene, String page, Boolean check_path, int colorR, int colorG, int colorB) {
        return getUnlimitWxaCode(token, scene, page, check_path, 0, colorR, colorG, colorB);
    }

    /**
     * 获取不限制的小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param scene 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符，其它字符请自行编码为合法字符（因不支持%，中文无法使用 urlencode 处理，请使用其他编码方式）
     * @param page 默认是主页，页面 page，例如 pages/index/index，根路径前不要填加 /，不能携带参数（参数请放在scene字段里），如果不填写这个字段，默认跳主页面。scancode_time为系统保留参数，不允许配置
     * @param width 默认430，二维码的宽度，单位 px，最小 280px，最大 1280px
     * @return 不限制的小程序码
     */
    public static WxBufferResult getUnlimitWxaCode(String token, String scene, String page, int width) {
        return getUnlimitWxaCode(token, scene, page, true, width);
    }

    /**
     * 获取不限制的小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param scene 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符，其它字符请自行编码为合法字符（因不支持%，中文无法使用 urlencode 处理，请使用其他编码方式）
     * @param page 默认是主页，页面 page，例如 pages/index/index，根路径前不要填加 /，不能携带参数（参数请放在scene字段里），如果不填写这个字段，默认跳主页面。scancode_time为系统保留参数，不允许配置
     * @param width 默认430，二维码的宽度，单位 px，最小 280px，最大 1280px
     * @param colorR auto_color 为 false 时生效，颜色R值，默认 0
     * @param colorG auto_color 为 false 时生效，颜色G值，默认 0
     * @param colorB auto_color 为 false 时生效，颜色B值，默认 0
     * @return 不限制的小程序码
     */
    public static WxBufferResult getUnlimitWxaCode(String token, String scene, String page, int width, int colorR, int colorG, int colorB) {
        return getUnlimitWxaCode(token, scene, page, true, width, colorR, colorG, colorB);
    }

    /**
     * 获取不限制的小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param scene 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符，其它字符请自行编码为合法字符（因不支持%，中文无法使用 urlencode 处理，请使用其他编码方式）
     * @param page 默认是主页，页面 page，例如 pages/index/index，根路径前不要填加 /，不能携带参数（参数请放在scene字段里），如果不填写这个字段，默认跳主页面。scancode_time为系统保留参数，不允许配置
     * @param check_path 默认是true，检查page 是否存在，为 true 时 page 必须是已经发布的小程序存在的页面（否则报错）；为 false 时允许小程序未发布或者 page 不存在， 但page 有数量上限（60000个）请勿滥用
     * @param width 默认430，二维码的宽度，单位 px，最小 280px，最大 1280px
     * @return 不限制的小程序码
     */
    public static WxBufferResult getUnlimitWxaCode(String token, String scene, String page, Boolean check_path, int width) {
        return getUnlimitWxaCode(token, scene, page, check_path, width, false);
    }

    /**
     * 获取不限制的小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param scene 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符，其它字符请自行编码为合法字符（因不支持%，中文无法使用 urlencode 处理，请使用其他编码方式）
     * @param page 默认是主页，页面 page，例如 pages/index/index，根路径前不要填加 /，不能携带参数（参数请放在scene字段里），如果不填写这个字段，默认跳主页面。scancode_time为系统保留参数，不允许配置
     * @param check_path 默认是true，检查page 是否存在，为 true 时 page 必须是已经发布的小程序存在的页面（否则报错）；为 false 时允许小程序未发布或者 page 不存在， 但page 有数量上限（60000个）请勿滥用
     * @param width 默认430，二维码的宽度，单位 px，最小 280px，最大 1280px
     * @param colorR auto_color 为 false 时生效，颜色R值，默认 0
     * @param colorG auto_color 为 false 时生效，颜色G值，默认 0
     * @param colorB auto_color 为 false 时生效，颜色B值，默认 0
     * @return 不限制的小程序码
     */
    public static WxBufferResult getUnlimitWxaCode(String token, String scene, String page, Boolean check_path, int width, int colorR, int colorG, int colorB) {
        return getUnlimitWxaCode(token, scene, page, check_path, width, colorR, colorG, colorB, false);
    }

    /**
     * 获取不限制的小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param scene 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符，其它字符请自行编码为合法字符（因不支持%，中文无法使用 urlencode 处理，请使用其他编码方式）
     * @param page 默认是主页，页面 page，例如 pages/index/index，根路径前不要填加 /，不能携带参数（参数请放在scene字段里），如果不填写这个字段，默认跳主页面。scancode_time为系统保留参数，不允许配置
     * @param check_path 默认是true，检查page 是否存在，为 true 时 page 必须是已经发布的小程序存在的页面（否则报错）；为 false 时允许小程序未发布或者 page 不存在， 但page 有数量上限（60000个）请勿滥用
     * @param width 默认430，二维码的宽度，单位 px，最小 280px，最大 1280px
     * @param isHyaline 默认是false，是否需要透明底色，为 true 时，生成透明底色的小程序
     * @return 不限制的小程序码
     */
    public static WxBufferResult getUnlimitWxaCode(String token, String scene, String page, Boolean check_path, int width, boolean isHyaline) {
        return getUnlimitWxaCode(token, scene, page, check_path, width, isHyaline, null);
    }

    /**
     * 获取不限制的小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param scene 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符，其它字符请自行编码为合法字符（因不支持%，中文无法使用 urlencode 处理，请使用其他编码方式）
     * @param page 默认是主页，页面 page，例如 pages/index/index，根路径前不要填加 /，不能携带参数（参数请放在scene字段里），如果不填写这个字段，默认跳主页面。scancode_time为系统保留参数，不允许配置
     * @param check_path 默认是true，检查page 是否存在，为 true 时 page 必须是已经发布的小程序存在的页面（否则报错）；为 false 时允许小程序未发布或者 page 不存在， 但page 有数量上限（60000个）请勿滥用
     * @param width 默认430，二维码的宽度，单位 px，最小 280px，最大 1280px
     * @param colorR auto_color 为 false 时生效，颜色R值，默认 0
     * @param colorG auto_color 为 false 时生效，颜色G值，默认 0
     * @param colorB auto_color 为 false 时生效，颜色B值，默认 0
     * @param isHyaline 默认是false，是否需要透明底色，为 true 时，生成透明底色的小程序
     * @return 不限制的小程序码
     */
    public static WxBufferResult getUnlimitWxaCode(String token, String scene, String page, Boolean check_path, int width, int colorR, int colorG, int colorB, boolean isHyaline) {
        return getUnlimitWxaCode(token, scene, page, check_path, width, colorR, colorG, colorB, isHyaline, null);
    }

    /**
     * 获取不限制的小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param scene 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符，其它字符请自行编码为合法字符（因不支持%，中文无法使用 urlencode 处理，请使用其他编码方式）
     * @param page 默认是主页，页面 page，例如 pages/index/index，根路径前不要填加 /，不能携带参数（参数请放在scene字段里），如果不填写这个字段，默认跳主页面。scancode_time为系统保留参数，不允许配置
     * @param check_path 默认是true，检查page 是否存在，为 true 时 page 必须是已经发布的小程序存在的页面（否则报错）；为 false 时允许小程序未发布或者 page 不存在， 但page 有数量上限（60000个）请勿滥用
     * @param width 默认430，二维码的宽度，单位 px，最小 280px，最大 1280px
     * @param isHyaline 默认是false，是否需要透明底色，为 true 时，生成透明底色的小程序
     * @param envVersion 要打开的小程序版本。正式版为 "release"，体验版为 "trial"，开发版为 "develop"。默认是正式版
     * @return 不限制的小程序码
     */
    public static WxBufferResult getUnlimitWxaCode(String token, String scene, String page, Boolean check_path, int width, boolean isHyaline, MpVersion envVersion) {
        return getUnlimitWxaCode(token, scene, page, check_path, width, true, 0, 0, 0, isHyaline, envVersion);
    }

    /**
     * 获取不限制的小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param scene 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符，其它字符请自行编码为合法字符（因不支持%，中文无法使用 urlencode 处理，请使用其他编码方式）
     * @param page 默认是主页，页面 page，例如 pages/index/index，根路径前不要填加 /，不能携带参数（参数请放在scene字段里），如果不填写这个字段，默认跳主页面。scancode_time为系统保留参数，不允许配置
     * @param check_path 默认是true，检查page 是否存在，为 true 时 page 必须是已经发布的小程序存在的页面（否则报错）；为 false 时允许小程序未发布或者 page 不存在， 但page 有数量上限（60000个）请勿滥用
     * @param width 默认430，二维码的宽度，单位 px，最小 280px，最大 1280px
     * @param colorR auto_color 为 false 时生效，颜色R值，默认 0
     * @param colorG auto_color 为 false 时生效，颜色G值，默认 0
     * @param colorB auto_color 为 false 时生效，颜色B值，默认 0
     * @param isHyaline 默认是false，是否需要透明底色，为 true 时，生成透明底色的小程序
     * @param envVersion 要打开的小程序版本。正式版为 "release"，体验版为 "trial"，开发版为 "develop"。默认是正式版
     * @return 不限制的小程序码
     */
    public static WxBufferResult getUnlimitWxaCode(String token, String scene, String page, Boolean check_path, int width, int colorR, int colorG, int colorB, boolean isHyaline, MpVersion envVersion) {
        return getUnlimitWxaCode(token, scene, page, check_path, width, false, colorR, colorG, colorB, isHyaline, envVersion);
    }

    /**
     * 获取不限制的小程序码（小程序）
     * 该接口用于获取小程序码，适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param scene 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符，其它字符请自行编码为合法字符（因不支持%，中文无法使用 urlencode 处理，请使用其他编码方式）
     * @param page 默认是主页，页面 page，例如 pages/index/index，根路径前不要填加 /，不能携带参数（参数请放在scene字段里），如果不填写这个字段，默认跳主页面。scancode_time为系统保留参数，不允许配置
     * @param check_path 默认是true，检查page 是否存在，为 true 时 page 必须是已经发布的小程序存在的页面（否则报错）；为 false 时允许小程序未发布或者 page 不存在， 但page 有数量上限（60000个）请勿滥用
     * @param width 默认430，二维码的宽度，单位 px，最小 280px，最大 1280px
     * @param autoColor 自动配置线条颜色，如果颜色依然是黑色，则说明不建议配置主色调，默认 false
     * @param colorR auto_color 为 false 时生效，颜色R值，默认 0
     * @param colorG auto_color 为 false 时生效，颜色G值，默认 0
     * @param colorB auto_color 为 false 时生效，颜色B值，默认 0
     * @param isHyaline 默认是false，是否需要透明底色，为 true 时，生成透明底色的小程序
     * @param envVersion 要打开的小程序版本。正式版为 "release"，体验版为 "trial"，开发版为 "develop"。默认是正式版
     * @return 不限制的小程序码
     */
    public static WxBufferResult getUnlimitWxaCode(String token, String scene, String page, Boolean check_path, int width, boolean autoColor, int colorR, int colorG, int colorB, boolean isHyaline, MpVersion envVersion) {
        JSONObject requestData = new JSONObject();
        requestData.put("scene", scene);
        if (page != null && page != "") requestData.put("page", page);
        if (width > 0) requestData.put("width", width);
        requestData.put("auto_color", autoColor);
        if (!autoColor) {
            JSONObject lineColor = new JSONObject();
            lineColor.put("r", colorR > 0 ? colorR : 0);
            lineColor.put("g", colorG > 0 ? colorG : 0);
            lineColor.put("b", colorB > 0 ? colorB : 0);
            requestData.put("line_color", lineColor);
        }
        requestData.put("is_hyaline", isHyaline);
        if (envVersion != null) requestData.put("env_version", envVersion.name());
        byte[] result = postByte(format(GET_UNLIMITED_WXA_CODE_URL, token), requestData.toJSONString(), WECHAT_AGENT);
        try {
            return JSONObject.parseObject(getText(result), WxBufferResult.class);
        } catch (Exception e) {
            WxBufferResult wxBufferResult = new WxBufferResult(0, "");
            wxBufferResult.setBuffer(result);
            return wxBufferResult;
        }
    }


    /**
     * 获取小程序二维码（小程序）
     * 获取小程序二维码，适用于需要的码数量较少的业务场景。通过该接口生成的小程序码，永久有效，有数量限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param path 扫码进入的小程序页面路径，最大长度 128 个字符，不能为空；对于小游戏，可以只传入 query 部分，来实现传参效果，如：传入 "?foo=bar"，即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}。scancode_time为系统保留参数，不允许配置
     * @return 小程序二维码
     */
    public static WxBufferResult getWxaQrcode(String token, String path) {
        return getWxaQrcode(token, path, 0);
    }

    /**
     * 获取小程序二维码（小程序）
     * 获取小程序二维码，适用于需要的码数量较少的业务场景。通过该接口生成的小程序码，永久有效，有数量限制
     * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html
     * @param token 接口调用凭据
     * @param path 扫码进入的小程序页面路径，最大长度 128 个字符，不能为空；对于小游戏，可以只传入 query 部分，来实现传参效果，如：传入 "?foo=bar"，即可在 wx.getLaunchOptionsSync 接口中的 query 参数获取到 {foo:"bar"}。scancode_time为系统保留参数，不允许配置
     * @param width 二维码的宽度，单位 px。最小 280px，最大 1280px;默认是430
     * @return 小程序二维码
     */
    public static WxBufferResult getWxaQrcode(String token, String path, int width) {
        JSONObject requestData = new JSONObject();
        requestData.put("path", path);
        if (width > 0) requestData.put("width", width);
        byte[] result = postByte(format(GET_WXA_QRCODE_URL, token), requestData.toJSONString(), WECHAT_AGENT);
        try {
            return JSONObject.parseObject(getText(result), WxBufferResult.class);
        } catch (Exception e) {
            WxBufferResult wxBufferResult = new WxBufferResult(0, "");
            wxBufferResult.setBuffer(result);
            return wxBufferResult;
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="虚拟支付">

    /**
     * 获取虚拟支付支付签名
     * @param uri 请求的API的uri部分，不带query_string
     * @param signData 签名数据
     * @param appKey 对应环境的AppKey
     * @return 支付签名
     */
    public static String getPaySig(String uri, String signData, String appKey) {
        String needSignMsg = uri + "&" + signData;
        return calculateHmacSHA256(needSignMsg, appKey);
    }

    /**
     * 获取虚拟支付用户态签名
     * @param signData 签名数据
     * @param sessionKey 当前用户有效的session_key
     * @return 用户态签名
     */
    public static String getSignature(String signData, String sessionKey) {
        return calculateHmacSHA256(signData, sessionKey);
    }

    /**
     * 使用HMAC-SHA256算法计算签名
     * @param data 待签名的数据
     * @param key 签名密钥
     * @return 十六进制的签名结果
     */
    private static String calculateHmacSHA256(String data, String key) {
        try {
            // 创建HMAC-SHA256实例
            Mac mac = Mac.getInstance("HmacSHA256");
            // 初始化密钥
            SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            mac.init(secretKey);
            // 计算签名
            byte[] signatureBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
            // 转换为十六进制字符串（兼容低版本Java）
            return bytesToHex(signatureBytes);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            throw new RuntimeException("计算签名失败", e);
        }
    }

    /**
     * 将字节数组转换为十六进制字符串（兼容Java 17以下版本）
     * @param bytes 字节数组
     * @return 十六进制字符串
     */
    private static String bytesToHex(byte[] bytes) {
        StringBuilder hexString = new StringBuilder();
        for (byte b : bytes) {
            // 将字节转换为十六进制表示
            String hex = Integer.toHexString(0xFF & b);
            // 确保每个字节转换为两位十六进制数
            if (hex.length() == 1) {
                hexString.append('0');
            }
            hexString.append(hex);
        }
        return hexString.toString();
    }

    // </editor-fold>

    // <editor-fold desc="私有方法">
}
